package net.stuffrepos.bricknetcenter.util.validation;

import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import net.stuffrepos.util.cache.CacheableMapValue;
import net.stuffrepos.util.cache.CacheableValue;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class Property {

    public static Property getProperty(Object entityInstance, String propertyName) {
        assert entityInstance != null;
        return getProperty(entityInstance.getClass(), propertyName);
    }
    private final String propertyName;
    private final Class entityClass;
    private static final Log log = LogFactory.getLog(Property.class);
    private static CacheableMapValue<Class, CacheableMapValue<String, Property>> properties = new CacheableMapValue<Class, CacheableMapValue<String, Property>>() {

        @Override
        protected CacheableMapValue<String, Property> calculate(final Class entityClass) {
            return new CacheableMapValue<String, Property>() {

                @Override
                protected Property calculate(final String propertyName) {
                    return new Property(entityClass, propertyName);
                }
            };
        }
    };
    private final CacheableValue<PropertyDescriptor> propertyDescriptor = new CacheableValue<PropertyDescriptor>() {

        @Override
        protected PropertyDescriptor calculate() {
            try {
                PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(entityInstance.getValue(), propertyName);
                if (descriptor != null) {
                    return descriptor;
                } else {
                    throw new RuntimeException("Descriptor not found to property " + Property.this);
                }
            } catch (IllegalAccessException ex) {
                throw new RuntimeException(Property.this.toString(), ex);
            } catch (InvocationTargetException ex) {
                throw new RuntimeException(Property.this.toString(), ex);
            } catch (NoSuchMethodException ex) {
                throw new RuntimeException(Property.this.toString(), ex);
            }
        }
    };
    private final CacheableValue<Map<Class<? extends Annotation>, Annotation>> annotations = new CacheableValue<Map<Class<? extends Annotation>, Annotation>>() {

        @Override
        protected Map<Class<? extends Annotation>, Annotation> calculate() {
            List<Annotation> annotations = new LinkedList<Annotation>();
            try {
                Field field = entityClass.getDeclaredField(propertyName);
                annotations.addAll(Arrays.asList(field.getAnnotations()));
            } catch (NoSuchFieldException ex) {
                log.trace(entityClass + "." + propertyName, ex);
            } catch (SecurityException ex) {
                log.trace(entityClass + "." + propertyName, ex);
            }
            if (propertyDescriptor.getValue().getReadMethod() != null) {
                annotations.addAll(Arrays.asList(propertyDescriptor.getValue().getReadMethod().getAnnotations()));
            }
            if (propertyDescriptor.getValue().getWriteMethod() != null) {
                annotations.addAll(Arrays.asList(propertyDescriptor.getValue().getWriteMethod().getAnnotations()));
            }
            Map<Class<? extends Annotation>, Annotation> map = new HashMap<Class<? extends Annotation>, Annotation>();
            for (Annotation a : annotations) {
                map.put(a.annotationType(), a);
            }
            return map;
        }
    };
    private final CacheableValue<Object> entityInstance = new CacheableValue<Object>() {

        @Override
        protected Object calculate() {
            try {
                return entityClass.newInstance();
            } catch (InstantiationException ex) {
                throw new RuntimeException(ex);
            } catch (IllegalAccessException ex) {
                throw new RuntimeException(ex);
            }
        }
    };

    public Property(Class entityClass, String propertyName) {
        this.propertyName = propertyName;
        this.entityClass = entityClass;
    }

    public Class getPropertyType() {
        return propertyDescriptor.getValue().getPropertyType();
    }
    /*
    public Class getPropertyType() {
    Class type = propertyDescriptor.getValue().getPropertyType();
    while (!(Object.class.equals(type) || Object.class.equals(type.getSuperclass()))) {
    type = type.getSuperclass();
    }
    return type;
    }*/

    public Constraint getConstraint() {
        return ConstraintFactory.getFromEntityProperty(this);
    }

    public static Property getProperty(Class entityClass, String propertyName) {
        assert entityClass != null;
        assert propertyName != null;
        return properties.getValue(entityClass).getValue(propertyName);
    }

    @Override
    public String toString() {
        return "Property(" + entityClass.getName() + "," + propertyName + ")";
    }

    public boolean hasAnnotation(Class<? extends Annotation> annotationClass) {
        return annotations.getValue().get(annotationClass) != null;
    }

    public Annotation getAnnotation(Class<? extends Annotation> annotationClass) {
        return annotations.getValue().get(annotationClass);
    }

    public String getPropertyName() {
        return this.propertyName;
    }

    public Object getObjectValue(Object object) {
        try {
            return PropertyUtils.getProperty(object, propertyName);
        } catch (IllegalAccessException ex) {
            throw new RuntimeException(ex);
        } catch (InvocationTargetException ex) {
            throw new RuntimeException(ex);
        } catch (NoSuchMethodException ex) {
            throw new RuntimeException(ex);
        }
    }

    public void setObjectValue(Object object, Object value) {

        try {
            PropertyUtils.setProperty(object, propertyName, value);
        } catch (IllegalAccessException ex) {
            throw new RuntimeException(ex);
        } catch (InvocationTargetException ex) {
            throw new RuntimeException(ex);
        } catch (NoSuchMethodException ex) {
            throw new RuntimeException(ex);
        }
    }
}
